﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using EntityBinder.Interface;

namespace EntityBinder.Xml
{
	class XmlReader : IEntityReader
	{
		private IEnumerable<IEntityElement> m_elementsRoot = null;

		internal XmlReader()
		{
			
		}

		internal XmlReader(IEntityElement _element)
		{
			m_elementsRoot = new List<IEntityElement>() { _element };
		}

		internal XmlReader(IEnumerable<IEntityElement> _elements)
		{
			m_elementsRoot = _elements;
		}

		public object Read(System.IO.Stream _inputStream)
		{
			return Read(_inputStream, m_elementsRoot);
		}

		public object Read(System.IO.Stream _inputStream, IEntityElement _element)
		{
			IEnumerable<IEntityElement> elements = new List<IEntityElement>() { _element };
			return Read(_inputStream, elements);
		}

		public object Read(System.IO.Stream _inputStream, IEnumerable<IEntityElement> _elements)
		{
			if(_elements == null || _elements.Count() == 0)
			{
				throw new EntityBinderException("XmlReader", "Read", "Can not read without any rules");
			}

			using (System.Xml.XmlReader reader = System.Xml.XmlReader.Create(_inputStream))
			{
				Dictionary<IEntityElement, bool> elementsOptional = new Dictionary<IEntityElement, bool>();
				IEntityElement currentElement = new EntityElement();
				object currentData = null;

				Stack<IEntityElement> elementsStack = new Stack<IEntityElement>();
				Stack<Dictionary<IEntityElement, bool>> elementsOptionalStack = new Stack<Dictionary<IEntityElement, bool>>();
				Stack<object> dataStack = new Stack<object>();

				foreach (IEntityElement e in _elements)
				{
					currentElement.AddChild(e);
				}

				bool continueWithoutRead = false;
				while (continueWithoutRead || reader.Read())
				{
					continueWithoutRead = false;
					if (reader.NodeType == System.Xml.XmlNodeType.Element)
					{
						IEnumerable<IEntityElement> children = from e in currentElement.Children
															   where e.Identifier == reader.Name
															   select e;
						if (children.Count() != 1)
						{
							throw new EntityBinderException("XmlReader", "Read", "Impossible to handle open tag " + reader.Name + " found " + children.Count() + " compatible rules");
						}
						IEntityElement child = children.Single();

						if (currentData != null) // do not stack the first element
						{
							if (!elementsOptional.ContainsKey(child))
							{
								elementsOptional.Add(child, true);
							}
							elementsOptionalStack.Push(elementsOptional);
							elementsOptional = new Dictionary<IEntityElement, bool>();

							dataStack.Push(currentData);
							elementsStack.Push(currentElement);
						}

						reader.Read();
						continueWithoutRead = !reader.HasValue;

						currentElement = child;
						currentData = child.Read(currentData, reader.Value);
					}
					else if (reader.NodeType == System.Xml.XmlNodeType.EndElement)
					{
						if (currentElement.Identifier == reader.Name)
						{
							if (dataStack.Count == 0)
							{
								return currentData;
							}

							IEnumerable<string> missing = from e in currentElement.Children
														  where !e.Optional && !elementsOptional[e]
														  select e.Identifier;
							if (missing.Count() > 0)
							{
								StringBuilder names = new StringBuilder();
								foreach (string s in missing)
								{
									names.Append(s);
									names.Append(" ");
								}

								throw new EntityBinderException("XmlReader", "Read", "Missing value for non optional tags : ");
							}

							currentElement = elementsStack.Pop();
							currentData = dataStack.Pop();
							elementsOptional = elementsOptionalStack.Pop();
						}
						else
						{
							throw new EntityBinderException("XmlReader", "Read", "Impossible to handle close tag " + reader.Name);
						}
					}
				}
			}
			return null;
		}

		public T Read<T>(System.IO.Stream _inputStream) where T : class
		{
			return Read(_inputStream) as T;
		}

		public T Read<T>(System.IO.Stream _inputStream, IEntityElement _element) where T : class
		{
			return Read(_inputStream, _element) as T;
		}

		public T Read<T>(System.IO.Stream _inputStream, IEnumerable<IEntityElement> _elements) where T : class
		{
			return Read(_inputStream, _elements) as T;
		}
	}
}
